using System;
using System.Collections.Generic;
using System.Diagnostics;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input.Touch;

// known limitations:
// * doesn't allow different transition times for different page transitions.
// * doesn't have transparencies


namespace SilverMenu
{
	public class TransitionData
	{
		public GUIElement Element;
		public Rectangle Rect;
		public Vector2 Position;
		public Vector2 StartPosition;
		public Vector2 EndPosition;
		public bool InTransition;
		public float CurTime;
		public float TransitionTime;
		public float FadeOutTime;
		public Color Color;
	}

	public class SilverMenuManager
	{
        public delegate void TransitionStart(Page curPage, Page nextPage);
        public event TransitionStart OnTransitionStart;

        public delegate void TransitionEnd(Page curPage);
        public event TransitionEnd OnTransitionEnd;

		/// <summary>
		/// Current page.
		/// </summary>
		private Page m_curPage;


		/// <summary>
		/// For each page element we create a transition.
		/// </summary>
		private List<TransitionData> m_transitions;
		private List<TransitionData> m_transitionsToRemove;

		private List<GUIElement> m_orphans;

		private Dictionary<String, SilverMenu> m_menus;

		private SilverMenu m_activeMenu;

		private SilverMenuContentManager m_contentManager;

		public SilverMenuManager(SilverMenuContentManager contentManager)
		{
			m_transitions = new List<TransitionData>();
			m_transitionsToRemove = new List<TransitionData>();
			m_orphans = new List<GUIElement>();
			m_menus = new Dictionary<String, SilverMenu>();

			m_contentManager = contentManager;
		}

		/// <summary>
		/// Loads a menu.
		/// </summary>
		/// <param name="menuName">Name of the menu to be loaded.</param>
        /// <returns>The Silver Menu that was just loaded.</returns>
        public SilverMenu LoadMenu(String menuName)
		{
			return LoadMenu(menuName, m_contentManager);
		}

		/// <summary>
		/// Loads a menu using a content manager other than the SilverMenuManager.
		/// </summary>
		/// <param name="menuName">Name of the menu to be loaded.</param>
		/// <param name="content">Content manager that will load the menu.</param>
        /// <returns>The Silver Menu that was just loaded.</returns>
        public SilverMenu LoadMenu(String menuName, SilverMenuContentManager content)
		{
			if (content == null)
				throw new InvalidOperationException("You must provide a valid Silver Menu Content Manager");

			SilverMenu menu = content.Load<SilverMenu>(menuName);
			m_menus.Add(menuName, menu);
			m_activeMenu = menu;

			// Enables flicks and/or tap gestures if necessary.
			if(menu.Pages != null)
			{
				foreach (var page in menu.Pages)
				{
					Page p = page.Value;
					foreach(var flick in p.Flicks)
					{
						if (!string.IsNullOrEmpty(flick))
							TouchPanel.EnabledGestures |= GestureType.Flick;
					}

					foreach (var elem in p.Elements)
					{
						if (elem is SilverButton && !String.IsNullOrEmpty(((SilverButton)elem).NextPage))
						{
							TouchPanel.EnabledGestures |= GestureType.Tap;
						}
					}
				}
			}

		    return menu;
		}

		public void UnloadMenu(String menuName)
		{
			//TODO: CHECK IF WE'RE ON A TRANSITION WITH THIS MENU.
			if (m_menus.ContainsKey(menuName))
				m_menus.Remove(menuName);
		}

		/// <summary>
		/// Prepares the menu manager to perform a transition.
		/// </summary>
		/// <param name="nextPageName">The new page name we will transit into.</param>
		public void DoTransition(String nextPageName)
		{
			if (m_activeMenu == null)
				return;

			if (m_activeMenu.Pages.ContainsKey(nextPageName))
				DoTransition(m_activeMenu.Pages[nextPageName]);
		}

		/// <summary>
		/// Prepares the menu manager to perform a transition.
		/// </summary>
		/// <param name="nextPage">The new page we will transit into.</param>
		public void DoTransition(Page nextPage)
		{
			if (nextPage == null)
				return;

			m_transitions = new List<TransitionData>();
			m_orphans = new List<GUIElement>();

			foreach (var elem in nextPage.Elements)
			{
				int i = 0;
				for(; m_curPage != null && i < m_curPage.Elements.Count; i++)
				{
					GUIElement oldElem = m_curPage.Elements[i];
					
					// Create transitions between shared elements.
					if (elem.Name == oldElem.Name)
					{
						TransitionData td = new TransitionData();
						td.StartPosition = oldElem.Position;
						td.Position = oldElem.Position;
						td.EndPosition = elem.Position;
						td.TransitionTime = elem.TransitionTime;

						td.Rect = oldElem.Rect;
						td.CurTime = 0;
						td.InTransition = true;
						td.Element = oldElem;

						m_transitions.Add(td);
						break;
					}
				}

				// Gather orfan elements from new page.
				if (m_curPage == null || i == m_curPage.Elements.Count)
					m_orphans.Add(elem);
			}

			// Gather orfan elements from previous page.
			if (m_curPage != null)
			{
				foreach (var elem in m_curPage.Elements)
				{
					int i = 0;
					for (; i < nextPage.Elements.Count; i++)
					{
						GUIElement oldElem = nextPage.Elements[i];

						if (elem.Name == oldElem.Name)
						{
							break;
						}
					}

					if (i == nextPage.Elements.Count)
						m_orphans.Add(elem);
				}
			}

			// Create transitions for orphan elements.
			foreach (var elem in m_orphans)
			{
				TransitionData td = new TransitionData();
				td.Position = elem.Position;
				td.StartPosition = elem.Position;
				td.EndPosition = elem.Position;
				td.Rect = elem.Rect;
				td.CurTime = 0;
				td.TransitionTime = elem.TransitionTime;
				td.InTransition = true;
				td.Element = elem;
				m_transitions.Add(td);
			}

            if (OnTransitionStart != null)
                OnTransitionStart(m_curPage, nextPage);

			m_curPage = nextPage;
		}

		public void Draw(SpriteBatch spriteBatch)
		{
			if (m_transitions.Count > 0)
			{
				foreach (var td in m_transitions)
				{
					td.Element.Draw(spriteBatch, ref td.Rect);
				}
			}
			else
			{
				foreach (var elem in m_curPage.Elements)
				{
					elem.Draw(spriteBatch, ref elem.Rect);
				}
			}
		}


		/// <summary>
		/// Updates all current active transitions.
		/// Note: Must be called when there are active transitions otherwise there's no need to call this method.
		/// </summary>
		/// <param name="delta">Time elapsed since last frame (in seconds).</param>
		/// <returns>True if there is no transition taking place, false otherwise.</returns>
		public bool Update(float delta, List<GestureSample> gestures)
		{
			bool ended = true;

			if (m_transitions != null && m_transitions.Count > 0)
			{
				// Do not let any gestures stay in the queue.
				while (TouchPanel.IsGestureAvailable)
				{
					GestureSample gs = TouchPanel.ReadGesture();
				}


				m_transitionsToRemove.Clear();
				foreach (var td in m_transitions)
				{
					bool res = UpdateTransitionData(td, delta);
					if (!res)
						ended = false;
					else
						m_transitionsToRemove.Add(td);
				}

				foreach (var td in m_transitionsToRemove)
				{
					m_transitions.Remove(td);
				}

                if (ended && OnTransitionEnd != null)
                {
                    OnTransitionEnd(m_curPage);
                }
            }
			else if(gestures != null && gestures.Count > 0 && m_curPage != null)
			{
				//GestureSample gs = TouchPanel.ReadGesture();
                foreach(GestureSample gs in gestures)
                {
                    if (gs.GestureType == GestureType.Flick)
                    {
                        FlickTypes ft;
                        if (Math.Abs(gs.Delta.X) > Math.Abs(gs.Delta.Y))
                        {
                            if (gs.Delta.X < 0)
                                ft = FlickTypes.Forward;
                            else
                                ft = FlickTypes.Back;
                        }
                        else
                        {
                            if (gs.Delta.Y < 0)
                                ft = FlickTypes.Up;
                            else
                                ft = FlickTypes.Down;
                        }

                        String nextPage = m_curPage.Flicks[(int)ft];
                        if (!string.IsNullOrEmpty(nextPage))
                        {
                            DoTransition(nextPage);
                        }
                        
                        break;
                    }
                    
                    if (gs.GestureType == GestureType.Tap)
                    {
                        foreach (var elem in m_curPage.Elements)
                        {
                            if (elem is SilverButton)
                            {
                                SilverButton btn = elem as SilverButton;
                                if (btn.Rect.Contains(new Point((int)gs.Position.X, (int)gs.Position.Y)))
                                {
                                    btn.InvokeOnClick();

                                    if (!string.IsNullOrEmpty(btn.NextPage))
                                    {
                                        DoTransition(btn.NextPage);
                                        break;
                                    }
                                }
                            }
                        }
                    }
                }
			}

            return ended;
        }

		//TODO: ADD SUPPORT TO EVENT HANDLERS BY XML

		public bool AddEventHandler(String menuName, String pageName, String btnName, SilverButton.OnClickDelegate handler)
		{
			if (m_menus.ContainsKey(menuName))
				return AddEventHandler(m_menus[menuName], pageName, btnName, handler);

			return false;
		}

		public bool AddEventHandler(SilverMenu menu, String pageName, String btnName, SilverButton.OnClickDelegate handler)
		{
			foreach (var page in menu.Pages)
			{
				if(page.Key == pageName)
					return AddEventHandler(page.Value, btnName, handler);
			}

			return false;
		}

		public bool AddEventHandler(String pageName, String btnName, SilverButton.OnClickDelegate handler)
		{
			if (m_activeMenu != null && m_activeMenu.Pages.ContainsKey(pageName))
				return AddEventHandler(m_activeMenu.Pages[pageName], btnName, handler);

			return false;
		}

		public bool AddEventHandler(Page page, String btnName, SilverButton.OnClickDelegate handler)
		{
			if (page == null)
				return false;

			foreach (var elem in page.Elements)
			{
				if (elem is SilverButton && elem.Name == btnName)
				{
					((SilverButton) elem).OnClick += handler;
					TouchPanel.EnabledGestures |= GestureType.Tap;
					return true;
				}
			}

			return false;
		}

		/// <summary>
		/// Updates a transition data. A transition data has information about position, time, color of a GUI element.
		/// When a transition between to different pages occurs, a transition data is created for each GUI element involved in both
		/// current and new pages.
		/// </summary>
		/// <param name="td">The transition data to be updated.</param>
		/// <param name="delta">The time that has passed since last update.</param>
		/// <returns></returns>
		public static bool UpdateTransitionData(TransitionData td, float delta)
		{
			bool ended = true;

			if (td.InTransition)
			{
				ended = false;
				td.CurTime += delta;

				// end of transition?
				if (td.CurTime >= td.TransitionTime)
				{
					td.CurTime = td.TransitionTime;
					td.InTransition = false;
					
					ended = true;
				}

				// update positions
				td.Position.X = QuadraticInOut(td.StartPosition.X, td.EndPosition.X - td.StartPosition.X, td.CurTime / td.TransitionTime);
				td.Rect.X = (int)QuadraticInOut(td.StartPosition.X, td.EndPosition.X - td.StartPosition.X, td.CurTime / td.TransitionTime);
			}

			return ended;
		}

		/// <summary>
		/// Do a smooth interpolation between two values.
		/// </summary>
		/// <param name="start">The start value of the property to update.</param>
		/// <param name="delta">The amount of change between start and end.</param>
		/// <param name="progress">Current progress, in seconds.</param>
		/// <returns></returns>
		public static float QuadraticInOut(float start, float delta, float progress)
		{
			if (progress < 0.5f)
			{
				float inProgress = progress * 2f;
				return (delta * 0.5f) * (inProgress * inProgress) + start;
			}
			
			float outProgress = (progress - 0.5f) * 2f;
			return -delta * 0.5f * (outProgress * (outProgress - 2f)) + start + delta*0.5f;
		} 
	}
}
